package com.yy.service;

import com.yy.utils.DistributeLockFactory;
import com.yy.utils.RedisDistributeLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @Author: chen
 * @Date: 2022/9/21 21:23
 * @Desc:
 */
@Service
public class RedisStockService {
    @Autowired
    StringRedisTemplate redisTemplate;

    @Autowired
    DistributeLockFactory distributeLockFactory;


    /**
     * hash结构和lua脚本解决可重入性的问题
     * 使用timer定时器和lua脚本实现自动续期功能
     */
    public void deduct() {

        RedisDistributeLock redisLock = distributeLockFactory.getRedisLock("lock");
        redisLock.lock();

        // 扣减库存
        try {
            String stock = redisTemplate.opsForValue().get("stock");

            if (null!=stock && stock.length()>0) {
                int count = Integer.parseInt(stock);
                if(count>0) {
                    redisTemplate.opsForValue().set("stock",String.valueOf(--count));
                }
            }

            // 测试自动续期
            /* try {
                TimeUnit.SECONDS.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } */

        } finally {
            redisLock.unlock();
        }
    }

    /**
     * 分布式锁,使用setnx指令实现
     * 问题:
     *   1. 死锁问题,加锁之后服务器宕机,添加过期时间解决
     *   2. 原子性,将获取锁和设置过期时间在一条指令内完成
     *      判断是否是自己的锁和删除锁也应该保证原子性
     *   3. 锁误删,只能删除自己的锁,通过判断value的值解决
     */
    public void deduct2() {
        String uuid = UUID.randomUUID().toString();

        // 获取锁
        while (!redisTemplate.opsForValue().setIfAbsent("lock", uuid,30,TimeUnit.SECONDS)) {
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        //扣减库存
        try {
            String stock = redisTemplate.opsForValue().get("stock");

            if (null!=stock && stock.length()>0) {
                int count = Integer.parseInt(stock);
                if(count>0) {
                   redisTemplate.opsForValue().set("stock",String.valueOf(--count));
                }
            }
        } finally {
            //删除锁
            String script="if redis.call('get',KEYS[1])==ARGV[1] \n" +
                "then \n" +
                "    return redis.call('del',KEYS[1])\n" +
                "else\n" +
                "    return 0\n" +
                "end";

            redisTemplate.execute(RedisScript.of(script,Boolean.class), Arrays.asList("lock"), uuid);
        }
    }

    /**
     * 使用redis乐观锁机制
     * watch: 监听一个或多个key的值
     * multi: 开启事务
     * exec: 执行事务,如果key的值发生变化,取消事务,反之则提交事物
     * 不使用decrement方法是为了模拟复杂业务场景
     * 问题:
     *   1. 性能低
     *   2. 还会出现连接不够用的情况
     */
    public void deduct1() {
        redisTemplate.execute(new SessionCallback<Object>() {
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                operations.watch("stock");

                String stock = operations.opsForValue().get("stock").toString();
                if (null != stock && stock.length() > 0) {
                    int count = Integer.parseInt(stock);
                    if (count > 0) {
                        operations.multi();
                        operations.opsForValue().set("stock", String.valueOf(--count));
                        List exec = operations.exec();
                        if (null == exec || exec.size() == 0) {
                            try {
                                TimeUnit.MILLISECONDS.sleep(100);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            deduct();

                            return exec;
                        }
                    }
                }
                return null;
            }
        });
    }

}
